﻿namespace PugLib.UI.Controls
{
	// Copyright © 2007 John M Rusk (http://dotnet.agilekiwi.com)
	// 
	// You may use this source code ("The Software") in any manner you wish, 
	// subject to the following conditions:
	//
	// (a) The above copyright notice and this permission notice shall be
	//     included in all copies or substantial portions of the Software.
	//
	// (b) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
	//     EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
	//     OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
	//     NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
	//     HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
	//     WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
	//     FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
	//     OTHER DEALINGS IN THE SOFTWARE.


	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Diagnostics.CodeAnalysis;
	using System.Drawing;
	using System.Drawing.Drawing2D;
	using System.Windows.Forms;

	using PugLib.Extensions;

	/// <summary>
	/// A Control that can be placed on a form
	/// </summary>
	public sealed class SparklineControl : Control
	{
		private readonly SparklineRenderer _renderer;

		public SparklineControl()
		{
			_renderer = new SparklineRenderer();
			_renderer.WidthChanged += RendererWidthChanged;

			DoubleBuffered = true;
		}

		[Browsable(true), EditorBrowsable(EditorBrowsableState.Always), DefaultValue(1)]
		public int PixelsPerElement
		{
			get { return _renderer.PixelsPerElement; }
			set { _renderer.PixelsPerElement = value; }
		}

		[Browsable(true), DesignerSerializationVisibility(DesignerSerializationVisibility.Visible), EditorBrowsable(EditorBrowsableState.Always)]
		public override Color BackColor
		{
			get { return _renderer.BackgroundColor; }
			set { _renderer.BackgroundColor = value; }
		}

		[Browsable(true), DesignerSerializationVisibility(DesignerSerializationVisibility.Visible), EditorBrowsable(EditorBrowsableState.Always)]
		public override Color ForeColor
		{
			get { return _renderer.LineColor; }
			set { _renderer.LineColor = value; }
		}

		[Browsable(true), DesignerSerializationVisibility(DesignerSerializationVisibility.Visible), EditorBrowsable(EditorBrowsableState.Always)]
		public Color LineColor
		{
			get { return _renderer.LineColor; }
			set { _renderer.LineColor = value; }
		}

		[SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly"),
		 RefreshProperties(RefreshProperties.Repaint), AttributeProvider(typeof(IListSource))]
		public ICollection<int?> DataSource
		{
			get { return _renderer.DataElements; }
			set
			{
				_renderer.BindElements(value);
				Invalidate();
			}
		}

		protected override void Dispose(bool disposing)
		{
			if (disposing)
			{
				_renderer.WidthChanged -= RendererWidthChanged;
			}
			base.Dispose(disposing);
		}

		/// <summary>
		/// Sync our width to that calculated by the renderer (it drives the width)
		/// </summary>
		private void RendererWidthChanged(object sender, EventArgs e)
		{
			Width = _renderer.Width;
		}

		/// <summary>
		/// Sync renderer height to our height (we drive the height)
		/// </summary>
		protected override void OnSizeChanged(EventArgs e)
		{
			base.OnSizeChanged(e);
			_renderer.Height = Height;
		}

		protected override void OnPaint(PaintEventArgs e)
		{
			_renderer.Render(e.Graphics);
		}
	}

	/// <summary>
	/// Renders a sparkline to a given <see cref="Graphics"/> object.  Can be used by
	/// <see cref="SparklineControl"/> and can also be used in other contexts.
	/// For info on sparklines, see http://www.edwardtufte.com/bboard/q-and-a-fetch-msg?msg_id=0001OR 
	/// </summary>
	public sealed class SparklineRenderer
	{
		private Color _backgroundColor = Color.White;
		private ICollection<int?> _dataElements = new List<int?>();
		private int _height;
		private Color _lineColor = Color.FromArgb(40, 40, 40);
		private int _pixelsPerElement;

		/// <summary>
		/// Number of pixels to use for each data element.  Width of control is automatically set to
		/// count of <see cref="DataElements"/> * <see cref="PixelsPerElement"/>
		/// </summary>
		public int PixelsPerElement
		{
			get { return _pixelsPerElement; }
			set
			{
				_pixelsPerElement = value;
				OnWidthChanged();
			}
		}

		/// <summary>
		/// Data displayed by the sparkline.
		/// </summary>
		public ICollection<int?> DataElements
		{
			get { return _dataElements; }
		}

		/// <summary>
		/// Height in pixels. Must be specified by user.
		/// </summary>
		public int Height
		{
			get { return _height; }
			set { _height = value; }
		}

		/// <summary>
		/// Width in pixels. Automatically set to count of <see cref="DataElements"/> * <see cref="PixelsPerElement"/>
		/// </summary>
		public int Width
		{
			get { return _dataElements.Count * PixelsPerElement; }
		}

		public Color LineColor
		{
			get { return _lineColor; }
			set { _lineColor = value; }
		}

		public Color BackgroundColor
		{
			get { return _backgroundColor; }
			set { _backgroundColor = value; }
		}

		public void BindElements(ICollection<int?> dataElements)
		{
			_dataElements = dataElements;
			OnWidthChanged();
		}

		public event EventHandler WidthChanged;

		private void OnWidthChanged()
		{
			WidthChanged.Raise(this, EventArgs.Empty);
		}

		/// <summary>
		/// Draw the sparkline
		/// </summary>
		public void Render(Graphics g)
		{
			g.Clear(BackgroundColor);

			SmoothingMode oldSmoothing = g.SmoothingMode;
			PixelOffsetMode oldOffsetMode = g.PixelOffsetMode;

			try
			{
				// these settings seem to give the best anti-aliasing
				g.SmoothingMode = SmoothingMode.HighQuality;
				g.PixelOffsetMode = PixelOffsetMode.Default;

				DoRender(g);
			}
			finally
			{
				g.SmoothingMode = oldSmoothing;
				g.PixelOffsetMode = oldOffsetMode;
			}
		}

		private void DoRender(Graphics g)
		{
			int? max;
			int? min;
			GetBounds(out max, out min);
			if (min == null || max == null)
			{
				return; // no range of values to draw
			}

			// force single pixel width (see http://www.bobpowell.net/single_pixel_lines.htm )
			using (Pen pen = new Pen(LineColor, -1))
			{
				float range = max.Value - min.Value;
				float multiplier = Height / range;

				Point? last = null;
				int x = 0;
				foreach (int? element in _dataElements)
				{
					Point? current;
					if (element == null)
					{
						current = null;
					}
					else
					{
						int y = (int)((element - min) * multiplier);
						y = Height - y; // flip y co-ord to standard PC-style coordinates
						current = new Point(x, y);
						if (last == null)
						{
							g.DrawEllipse(pen, current.Value.X, current.Value.Y, 1, 1); // draw "one pixel"
						}
						else
						{
							g.DrawLine(pen, last.Value, current.Value);
						}
					}
					last = current;
					x += PixelsPerElement;
				}
			}
		}

		private void GetBounds(out int? max, out int? min)
		{
			min = null;
			max = null;
			foreach (int? element in _dataElements)
			{
				if (element < (min ?? int.MaxValue))
				{
					min = element;
				}
				if (element > (max ?? int.MinValue))
				{
					max = element;
				}
			}
			if (min > 0)
			{
				min = 0; // in this implementation, we are always tying our sparkline "y axis" to 0 at the bottom, if min is positive
			}
		}
	}
}